/*
 * Created on Mar 20, 2004
 * Created by Alon Rohter
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 */
package org.gudy.azureus2.core3.peer.util;

import java.net.InetAddress;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.peer.PEPeer;
import org.gudy.azureus2.core3.util.AENetworkClassifier;
import org.gudy.azureus2.core3.util.CRC32C;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.HostNameToIPResolver;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.plugins.peers.Peer;
import org.gudy.azureus2.plugins.utils.LocationProvider;
import org.gudy.azureus2.pluginsimpl.local.PluginCoreUtils;

import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;

/**
 * Varies peer connection utility methods.
 */
public class PeerUtils {

    private static final String CONFIG_MAX_CONN_PER_TORRENT = "Max.Peer.Connections.Per.Torrent";
    private static final String CONFIG_MAX_CONN_TOTAL = "Max.Peer.Connections.Total";

    public static int MAX_CONNECTIONS_PER_TORRENT;
    public static int MAX_CONNECTIONS_TOTAL;

    static {

        COConfigurationManager.addParameterListener(CONFIG_MAX_CONN_PER_TORRENT, new ParameterListener() {
            public void parameterChanged(String parameterName) {
                MAX_CONNECTIONS_PER_TORRENT = COConfigurationManager.getIntParameter(CONFIG_MAX_CONN_PER_TORRENT);
            }
        });

        MAX_CONNECTIONS_PER_TORRENT = COConfigurationManager.getIntParameter(CONFIG_MAX_CONN_PER_TORRENT);

        COConfigurationManager.addParameterListener(CONFIG_MAX_CONN_TOTAL, new ParameterListener() {
            public void parameterChanged(String parameterName) {
                MAX_CONNECTIONS_TOTAL = COConfigurationManager.getIntParameter(CONFIG_MAX_CONN_TOTAL);
            }
        });

        MAX_CONNECTIONS_TOTAL = COConfigurationManager.getIntParameter(CONFIG_MAX_CONN_TOTAL);
    }

    private static final NetworkAdmin network_admin;

    private static volatile long na_last_ip4_time;
    private static volatile long na_last_ip6_time;
    private static volatile byte[] na_last_ip4;
    private static volatile byte[] na_last_ip6;

    private static int na_tcp_port;

    static {
        NetworkAdmin temp = null;

        try {
            temp = NetworkAdmin.getSingleton();

        } catch (Throwable e) {
        }

        network_admin = temp;

        COConfigurationManager.addAndFireParameterListener("TCP.Listen.Port", new ParameterListener() {
            public void parameterChanged(String parameterName) {
                na_tcp_port = COConfigurationManager.getIntParameter(parameterName);
            }
        });
    }

    public static int getPeerPriority(String address, int port) {
        if (network_admin == null) {

            return (0);
        }

        try {

            InetAddress ia = HostNameToIPResolver.syncResolve(address);

            if (ia != null) {

                return (getPeerPriority(ia, port));
            }

        } catch (Throwable e) {
        }

        return (0);
    }

    public static int getPeerPriority(InetAddress address, int peer_port) {
        return (getPeerPriority(address.getAddress(), peer_port));
    }

    public static int getPeerPriority(byte[] peer_address, short peer_port) {
        return (getPeerPriority(peer_address, ((int) peer_port) & 0xffff));
    }

    public static int getPeerPriority(byte[] peer_address, int peer_port) {
        if (network_admin == null) {

            return (0);
        }

        if (peer_address == null) {

            return (0);
        }

        byte[] my_address = null;

        long now = SystemTime.getMonotonousTime();

        if (peer_address.length == 4) {

            if (na_last_ip4 != null && now - na_last_ip4_time < 120 * 1000) {

                my_address = na_last_ip4;

            } else {

                if (na_last_ip4_time == 0 || now - na_last_ip4_time > 10 * 1000) {

                    na_last_ip4_time = now;

                    InetAddress ia = network_admin.getDefaultPublicAddress(true);

                    if (ia != null) {

                        byte[] iab = ia.getAddress();

                        if (iab != null) {

                            na_last_ip4 = my_address = ia.getAddress();
                        }
                    }
                }
            }

            if (my_address == null) {

                my_address = na_last_ip4;
            }
        } else if (peer_address.length == 16) {

            if (na_last_ip6 != null && now - na_last_ip6_time < 120 * 1000) {

                my_address = na_last_ip6;

            } else {

                if (na_last_ip6_time == 0 || now - na_last_ip6_time > 10 * 1000) {

                    na_last_ip6_time = now;

                    InetAddress ia = network_admin.getDefaultPublicAddressV6(true);

                    if (ia != null) {

                        byte[] iab = ia.getAddress();

                        if (iab != null) {

                            na_last_ip6 = my_address = ia.getAddress();
                        }
                    }
                }
            }

            if (my_address == null) {

                my_address = na_last_ip6;
            }
        } else {

            return (0);
        }

        if (my_address != null && my_address.length == peer_address.length) {

            return (getPeerPriority(my_address, na_tcp_port, peer_address, peer_port));

        } else {

            return (0);
        }
    }

    private static int getPeerPriority(byte[] a1, int port1, byte[] a2, int port2) {
        // http://www.bittorrent.org/beps/bep_0040.html

        byte[] a1_masked = new byte[a1.length];
        byte[] a2_masked = new byte[a2.length];

        /*
         * The formula to be used in prioritizing peers is this: priority = crc32-c(sort(masked_client_ip, masked_peer_ip)) If the IP addresses are
         * the same, the port numbers (16-bit integers) should be used instead: priority = crc32-c(sort(client_port, peer_port)) For an IPv4 address,
         * the mask to be used should be FF.FF.55.55 unless the IP addresses are in the same /16. In that case, the mask to be used should be
         * FF.FF.FF.55. If the IP addresses are in the same /24, the entire address should be used (mask FF.FF.FF.FF). For an IPv6 address, the mask
         * should be derived in the same way, beginning with FFFF:FFFF:FFFF:5555:5555:5555:5555:5555. If the IP addresses are in the same /48, the
         * mask to be used should be FFFF:FFFF:FFFF:FF55:5555:5555:5555:5555. If the IP addresses are in the same /56, the mask to be used should be
         * FFFF:FFFF:FFFF:FFFF:5555:5555:5555:5555, etc...
         */

        int x = a1_masked.length == 4 ? 1 : 5;

        boolean same = true;

        int order = 0;

        for (int i = 0; i < a1_masked.length; i++) {
            byte a1_byte = a1[i];
            byte a2_byte = a2[i];

            if (i < x || same) {
                a1_masked[i] = a1_byte;
                a2_masked[i] = a2_byte;
            } else {
                a1_masked[i] = (byte) (a1_byte & 0x55);
                a2_masked[i] = (byte) (a2_byte & 0x55);
            }

            if (i >= x && same) {
                same = a1_byte == a2_byte;
            }

            if (order == 0) {

                order = (a1_masked[i] & 0xff) - (a2_masked[i] & 0xff);
            }
        }

        if (same) {

            a1_masked = new byte[] { (byte) (port1 >> 8), (byte) port1 };
            a2_masked = new byte[] { (byte) (port2 >> 8), (byte) port2 };

            order = port1 - port2;
        }

        CRC32C crc32 = new CRC32C();

        if (order < 0) {

            crc32.updateWord(a1_masked, true);
            crc32.updateWord(a2_masked, true);

        } else {

            crc32.updateWord(a2_masked, true);
            crc32.updateWord(a1_masked, true);
        }

        long res = crc32.getValue();

        return ((int) res);
    }

    /**
     * Get the number of new peer connections allowed for the given data item, within the configured per-torrent and global connection limits.
     * 
     * @return max number of new connections allowed, or -1 if there is no limit
     */
    public static int numNewConnectionsAllowed(PeerIdentityDataID data_id, int specific_max) {
        int curConnPerTorrent = PeerIdentityManager.getIdentityCount(data_id);
        int curConnTotal = PeerIdentityManager.getTotalIdentityCount();

        // specific max here will default to the global per-torrent default if not explicitly set
        // so we don't need to consider CONFIG_MAX_CONN_PER_TORRENT seperately

        int PER_TORRENT_LIMIT = specific_max;

        int perTorrentAllowed = -1; // default unlimited
        if (PER_TORRENT_LIMIT != 0) { // if limited
            int allowed = PER_TORRENT_LIMIT - curConnPerTorrent;
            if (allowed < 0)
                allowed = 0;
            perTorrentAllowed = allowed;
        }

        int totalAllowed = -1; // default unlimited
        if (MAX_CONNECTIONS_TOTAL != 0) { // if limited
            int allowed = MAX_CONNECTIONS_TOTAL - curConnTotal;
            if (allowed < 0)
                allowed = 0;
            totalAllowed = allowed;
        }

        int allowed = -1; // default unlimited
        if (perTorrentAllowed > -1 && totalAllowed > -1) { // if both limited
            allowed = Math.min(perTorrentAllowed, totalAllowed);
        } else if (perTorrentAllowed == -1 || totalAllowed == -1) { // if either unlimited
            allowed = Math.max(perTorrentAllowed, totalAllowed);
        }

        return allowed;
    }

    private static Set<Integer> ignore_peer_ports = new HashSet<Integer>();

    static {
        COConfigurationManager.addParameterListener("Ignore.peer.ports", new ParameterListener() {
            public void parameterChanged(String parameterName) {
                readIgnorePeerPorts();
            }
        });

        readIgnorePeerPorts();
    }

    private static void readIgnorePeerPorts() {
        // XXX Optimize me for ranges!!
        String str = COConfigurationManager.getStringParameter("Ignore.peer.ports").trim();

        ignore_peer_ports.clear();

        if (str.length() > 0) {

            String[] ports = str.split("\\;");
            if (ports != null && ports.length > 0) {
                for (int i = 0; i < ports.length; i++) {
                    String port = ports[i];
                    int spreadPos = port.indexOf('-');
                    if (spreadPos > 0 && spreadPos < port.length() - 1) {
                        try {
                            int iMin = Integer.parseInt(port.substring(0, spreadPos).trim());
                            int iMax = Integer.parseInt(port.substring(spreadPos + 1).trim());

                            iMin = Math.max(0, iMin);
                            iMax = Math.min(65535, iMax);

                            for (int j = iMin; j <= iMax; j++) {
                                ignore_peer_ports.add(Integer.valueOf(j));
                            }
                        } catch (Throwable e) {
                            Debug.out("Invalid ignore-port entry: " + port);
                        }
                    } else {
                        try {
                            ignore_peer_ports.add(Integer.parseInt(port.trim()));
                        } catch (Throwable e) {
                            Debug.out("Invalid ignore-port entry: " + port);
                        }
                    }
                }
            }
        }
    }

    public static boolean ignorePeerPort(int port) {
        return (ignore_peer_ports.contains(port));
    }

    static final String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

    public static byte[] createPeerID() {
        byte[] peerId = new byte[20];

        byte[] version = Constants.VERSION_ID;

        for (int i = 0; i < 8; i++) {
            peerId[i] = version[i];
        }

        for (int i = 8; i < 20; i++) {
            int pos = (int) (Math.random() * chars.length());
            peerId[i] = (byte) chars.charAt(pos);
        }

        // System.out.println( "generated new peer id:" + ByteFormatter.nicePrint(peerId));

        return (peerId);
    }

    public static byte[] createWebSeedPeerID() {
        byte[] peerId = new byte[20];

        peerId[0] = '-';
        peerId[1] = 'W';
        peerId[2] = 'S';

        for (int i = 3; i < 20; i++) {
            int pos = (int) (Math.random() * chars.length());
            peerId[i] = (byte) chars.charAt(pos);
        }

        // System.out.println( "generated new peer id:" + ByteFormatter.nicePrint(peerId));

        return (peerId);
    }

    private static volatile LocationProvider country_provider;
    private static long country_provider_last_check;

    private static Object country_key = new Object();

    private static LocationProvider getCountryProvider() {
        if (country_provider != null) {

            if (country_provider.isDestroyed()) {

                country_provider = null;
                country_provider_last_check = 0;
            }
        }

        if (country_provider == null) {

            long now = SystemTime.getMonotonousTime();

            if (country_provider_last_check == 0 || now - country_provider_last_check > 20 * 1000) {

                country_provider_last_check = now;

                java.util.List<LocationProvider> providers =
                        AzureusCoreFactory.getSingleton().getPluginManager().getDefaultPluginInterface().getUtilities().getLocationProviders();

                for (LocationProvider provider : providers) {

                    if (provider.hasCapabilities(LocationProvider.CAP_ISO3166_BY_IP | LocationProvider.CAP_COUNTY_BY_IP)) {

                        country_provider = provider;
                    }
                }
            }
        }

        return (country_provider);
    }

    public static String[] getCountryDetails(Peer peer) {
        return (getCountryDetails(PluginCoreUtils.unwrap(peer)));
    }

    public static String[] getCountryDetails(PEPeer peer) {
        if (peer == null) {

            return (null);
        }

        String[] details = (String[]) peer.getUserData(country_key);

        if (details == null) {

            LocationProvider lp = getCountryProvider();

            if (lp != null) {

                try {
                    String ip = peer.getIp();

                    if (HostNameToIPResolver.isDNSName(ip)) {

                        InetAddress peer_address = HostNameToIPResolver.syncResolve(ip);

                        String code = lp.getISO3166CodeForIP(peer_address);
                        String name = lp.getCountryNameForIP(peer_address, Locale.getDefault());

                        if (code != null && name != null) {

                            details = new String[] { code, name };

                        } else {

                            details = new String[0];
                        }
                    } else {

                        String cat = AENetworkClassifier.categoriseAddress(ip);

                        if (cat != AENetworkClassifier.AT_PUBLIC) {

                            details = new String[] { cat, cat };

                        } else {

                            details = new String[0];
                        }
                    }

                    peer.setUserData(country_key, details);

                } catch (Throwable e) {
                }
            }
        }

        return (details);
    }

    /*
     * public static void main( String[] args ) { // If the client is 123.213.32.10 and the peer is 98.76.54.32, the hash that they should both arrive
     * at is crc32-c(624C14007BD50000) or BB97323E. // If the client is 123.213.32.10 and the peer is 123.213.32.234, the hash that they should both
     * arrive at is crc32-c[(7BD5200A7BD520EA) or C816B840. CRC32C crc = new CRC32C();
     * crc.update("The quick brown fox jumps over the lazy dog".getBytes()); System.out.println( Long.toString( crc.getValue(), 16 )); try{ if (
     * getPeerPriority( InetAddress.getByName( "123.213.32.10" ).getAddress(), 10, InetAddress.getByName( "98.76.54.32" ).getAddress(), 10 ) !=
     * 0xBB97323E ){ System.out.println( "derp1" ); } if ( getPeerPriority( InetAddress.getByName( "123.213.32.10" ).getAddress(), 10,
     * InetAddress.getByName( "123.213.32.234" ).getAddress(), 10 ) != 0xC816B840 ){ System.out.println( "derp2" ); } if ( getPeerPriority(
     * InetAddress.getByName( "123.213.32.10" ).getAddress(), 10, InetAddress.getByName( "123.213.32.10" ).getAddress(), 20 ) != 1879809021 ){
     * System.out.println( "derp3" ); } }catch( Throwable e ){ e.printStackTrace(); } }
     */
}
